package typistcal.commandlogic;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import typistcal.exception.WrongTimeFormatException;
import typistcal.executor.Utility;
import typistcal.userinterface.UserInterface;

//@author A0096911Y
public class TimeUtility {
	private static final Logger LOGGER = Logger.getLogger(TimeUtility.class.getName());
	private static final String DELIM_PARSE = "-:,./\\|_ ";
	private static final String FORMAT_DATE = "d MMMM yyyy";
	private static final String FORMAT_TIME = "HH:mm";
	private static final String FORMAT_DATE_AND_TIME = FORMAT_DATE + "," + FORMAT_TIME;
	private static final int[] NUMBER_OF_DAYS_IN_MONTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	private static final String[] LIST_TODAY = {"today", "2day", "2dae", "todae", "tonight", "2nite", "2night", "tonite"};
	private static final String[] LIST_TOMORROW = {"tomorrow", "tmr", "tml", "2moro"};	
	private static final String[] LIST_MON = {"monday", "mon"};
	private static final String[] LIST_TUES = {"tuesday", "tue", "tues"};
	private static final String[] LIST_WED = {"wednesday", "wed"};
	private static final String[] LIST_THURS = {"thursday", "thurs", "thur"};
	private static final String[] LIST_FRI = {"friday", "fri"};
	private static final String[] LIST_SAT = {"saturday", "sat"};
	private static final String[] LIST_SUN = {"sunday", "sun"};
	private static final String[] LIST_DAY = {"monday", "mon", "tuesday", "tue", "tues","wednesday", "wed","thursday", "thurs", "thur","friday", "fri","saturday", "sat","sunday", "sun"};
	private static final String[] LIST_MONTH = {"jan", "january", "feb", "february", "mar", "march", "apr", "april", "may", "jun", "june", "jul", "july", "aug", "august", "sep", "september", "oct","october", "nov", "november", "dec", "december"};
	private static Calendar cal = Calendar.getInstance(Locale.ENGLISH);
	public enum DayOfWeek {
		MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
	}
	public enum Month {
		JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
	}
	
	public TimeUtility() {}
	
	public static String getDateAndTime() {
		SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_DATE + " " + FORMAT_TIME);
		Date date = getUpdatedCal().getTime();
		
		return formatter.format(date);
	}
	
	public static String getToday() {
		return getToday(FORMAT_DATE);
	}
	
	public static String getToday(String format) {
		SimpleDateFormat formatter = new SimpleDateFormat(format);
		Date date = getUpdatedCal().getTime();
		
		return formatter.format(date);
	}
	
	public static String getTomorrow() {
		return getDateOfDayFromToday(1, true);
	}
	
	public static String getDateOfDayFromToday(int numOfDays, boolean isFuture) {
		return getDateOfDayFromToday(FORMAT_DATE, numOfDays, isFuture);
	}
	
	public static String getDateOfDayFromToday(String format, int numOfDays, boolean isFuture) {
		SimpleDateFormat formatter = new SimpleDateFormat(format);
		Calendar tcal = getUpdatedCal();
		
		if ( !isFuture) {
			numOfDays *= -1;
		}
		
		tcal.add(Calendar.DAY_OF_MONTH, numOfDays);
		
		Date date = tcal.getTime();
		
		return formatter.format(date);
	}
	
	public static String parseDate(String primitiveDateFormat) throws WrongTimeFormatException {
		assert (primitiveDateFormat != null);
		assert (primitiveDateFormat != "");
		System.out.println("parseDate: " + primitiveDateFormat);
		String day, monthWord, monthNum, year;
		StringTokenizer st = new StringTokenizer(primitiveDateFormat, DELIM_PARSE);

		if ( st.countTokens() == 1 ) {
			String word = st.nextToken();

			if ( Utility.isStringInList(word, LIST_TODAY) ) {
				return getToday();
			} else if ( Utility.isStringInList(word, LIST_TOMORROW) ) {
				return getDateOfDayFromToday(1, true);
			} else {
				return findNextDay(assignDayOfWeek(word));
			}
		} else if ( st.countTokens() == 2 ) {
			String token1 = st.nextToken();
			String token2 = st.nextToken();

			if ( Utility.isStringInList(token1, LIST_MONTH) ) {
				monthNum = parseMonth(token1);
				monthWord = assignMonth(monthNum).toString();
				day = token2;
			} else if ( Utility.isStringInList(token2, LIST_MONTH) ) {
				monthNum = parseMonth(token2);
				monthWord = assignMonth(monthNum).toString();
				day = token1;
			} else if ( Utility.isStringInList(token2, LIST_DAY) ) {
				if ( token1.toLowerCase().equals("this") ) {
					return findNextDay(assignDayOfWeek(token2));
				} else if ( token1.toLowerCase().equals("next") ) {
					return getNext(assignDayOfWeek(token2));
				} else {
					LOGGER.log(Level.SEVERE, token1 + " <day> is not supported");
					throw new WrongTimeFormatException(token1 + " <day> is not supported");
				}
			} else {
				monthNum = token1;
				monthWord = assignMonth(monthNum).toString();
				day = token2;
			}

			try {
				year = assignYear(Integer.parseInt(day), Integer.parseInt(monthNum));
				if (!isDateValid(Integer.parseInt(day), Integer.parseInt(monthNum), Integer.parseInt(year))) {
					LOGGER.log(Level.SEVERE, primitiveDateFormat + " Date not valid");
					throw new WrongTimeFormatException("Invalid date!");
				}
			} catch (NumberFormatException e) {
				throw new WrongTimeFormatException("Invalid date!");
			}

			return new String(day + " " + Utility.capitaliseWord(monthWord)	+ " " + year);
		} else if ( st.countTokens() == 3 ) {
			day = st.nextToken();
			String token = st.nextToken();
			year = st.nextToken();
			if ( !Utility.isInteger(day) && !Utility.isInteger(year) ) {
				LOGGER.log(Level.SEVERE, primitiveDateFormat + "Invalid format for year/day!");
				throw new WrongTimeFormatException("Invalid format for year/day!");
			}

			if ( Utility.isStringInList(token, LIST_MONTH) ) {
				monthNum = parseMonth(token);
				monthWord = assignMonth(monthNum).toString();
			} else if ( Utility.isInteger(token) ) {
				monthNum = token;
				monthWord = assignMonth(monthNum).toString();
			} else {
				// invalid;
				throw new WrongTimeFormatException("Invalid format for month!");
			}

			try {
				if ( !isDateValid( Integer.parseInt(day), Integer.parseInt(monthNum), Integer.parseInt(year) ) ) {
					LOGGER.log(Level.SEVERE, primitiveDateFormat + " Invalid date! Is your year more than 1900?");
					throw new WrongTimeFormatException("Invalid date!");
				}
			} catch (NumberFormatException e) {
				throw new WrongTimeFormatException("Invalid date!");
			}

			return new String(day + " " + Utility.capitaliseWord(monthWord) + " " + year);
		} else {
			LOGGER.log(Level.SEVERE, primitiveDateFormat + " Date in wrong format! I don't know what you are trying there...");
			throw new WrongTimeFormatException("Invalid format for date!");
		}
	}

	private static String parseMonth(String month) throws WrongTimeFormatException {
		month = month.toLowerCase();
		switch (month) {
		case "jan": case "january":
			return "1";
		case "feb": case "february":
			return "2";
		case "mar": case "march":
			return "3";
		case "apr": case "april":
			return "4";
		case "may":
			return "5";
		case "jun": case "june":
			return "6";
		case "jul": case "july":
			return "7";
		case "aug": case "august":
			return "8";
		case "sep": case "september":
			return "9";
		case "oct": case "october":
			return "10";
		case "nov": case "november":
			return "11";
		case "dec": case "december":
			return "12";
		default:
			throw new WrongTimeFormatException("Invalid input for parseMonth!");
		}
	}

	private static String findNextDay(DayOfWeek target) throws WrongTimeFormatException {
		int today = getUpdatedCal().get(Calendar.DAY_OF_WEEK);
		int diff;
		
		diff = (convertDayOfWeekToInt(target) - today) % 7;

		if ( diff == 0 ) {
			String feedback;
			UserInterface.displayToInteractiveArea("Do you mean " + "\n" 
													+ "1. " + Utility.capitaliseWord(target.toString()) + ", " + getDateOfDayFromToday(0, true) + "\n"
													+ "2. " + Utility.capitaliseWord(target.toString()) + ", " + getDateOfDayFromToday(7, true));
			while ( true ) {
				feedback = UserInterface.displayAndAskForFeedback(null);
				if ( feedback.trim().equals("1") ) {
					return getToday();
				} else if ( feedback.trim().equals("2") ) {
					return getDateOfDayFromToday(7, true);
				} else {
					UserInterface.displayToDisplayArea("Invalid input! Please key either 1 or 2");
				}
			}
		}
		
		if ( diff < 0) {
			diff += 7;
		}
		
		return getDateOfDayFromToday(diff, true);
	}
	
	private static int convertDayOfWeekToInt(DayOfWeek day) throws WrongTimeFormatException {
		switch (day) {
		case SUNDAY:
			return 1;
		case MONDAY:
			return 2;
		case TUESDAY:
			return 3;
		case WEDNESDAY:
			return 4;
		case THURSDAY:
			return 5;
		case FRIDAY:
			return 6;
		case SATURDAY:
			return 7;
		default:
			throw new WrongTimeFormatException("invalid! not supposed to happen though hmm");
		}
	}
	
	private static String getNext(DayOfWeek day) throws WrongTimeFormatException {
		int diff;
		int today = getUpdatedCal().get(Calendar.DAY_OF_WEEK);
		int target = convertDayOfWeekToInt(day);
		
		if ( today == 1 ) {
			if ( target == 1 || target == 2 ) {
				diff =  7 + ( target - today );
			} else {
				diff = target - today;
				String feedback;
				
				UserInterface.displayToInteractiveArea("Do you mean " + "\n" 
						+ "1. " + Utility.capitaliseWord(day.toString()) + ", " + getDateOfDayFromToday(diff, true) + "\n"
						+ "2. " + Utility.capitaliseWord(day.toString()) + ", " + getDateOfDayFromToday(diff + 7, true));
				
				while ( true ) {
					feedback = UserInterface.displayAndAskForFeedback(null);
					if ( feedback.trim().equals("1") ) {
						return getDateOfDayFromToday(diff, true);
					} else if ( feedback.trim().equals("2") ) {
						return getDateOfDayFromToday(diff + 7, true);
					} else {
						UserInterface.displayToDisplayArea("Invalid input! Please key either 1 or 2");
					}
				}
			}
		} else {
			diff = 7 + ( target - today );
		}
		
		return getDateOfDayFromToday(diff, true);
	}

	private static String assignYear(int day, int month) {
		Calendar cal = Calendar.getInstance(); 
		
		if ( (cal.get(Calendar.MONTH) + 1 ) < month || ( cal.get( Calendar.MONTH) + 1) == month && cal.get(Calendar.DAY_OF_MONTH) <= day ) {
			return (new Integer( cal.get(Calendar.YEAR) )).toString();
		}
		else {
			return (new Integer( cal.get(Calendar.YEAR) + 1 )).toString();
		}
	}

	public static String parseTime(String primitiveTimeFormat) throws WrongTimeFormatException {
		String hour, minute;
		boolean isPm = false;
		boolean isAm = false;
		
		primitiveTimeFormat = primitiveTimeFormat.toLowerCase();
		if ( primitiveTimeFormat.endsWith("pm") ) {
			isPm = true;
			primitiveTimeFormat = primitiveTimeFormat.replaceAll("pm", "");
		}
		if ( primitiveTimeFormat.endsWith("am") ) {
			isAm = true;
			primitiveTimeFormat = primitiveTimeFormat.replaceAll("am", "");
		}
		
		StringTokenizer st = new StringTokenizer(primitiveTimeFormat, DELIM_PARSE);

		if ( st.countTokens() == 1 ) {
			hour = st.nextToken();
			minute = "00";
		} else if ( st.countTokens() == 2 ) {
			hour = st.nextToken();
			minute = st.nextToken();			
		} else {
			LOGGER.log(Level.SEVERE, primitiveTimeFormat + " Time in wrong format");
			throw new WrongTimeFormatException("Invalid format for time!");
		}

		try {
			if ( !isTimeValid( Integer.parseInt(hour), Integer.parseInt(minute), isAm, isPm ) ) {
				LOGGER.log(Level.SEVERE, primitiveTimeFormat + " Time is not valid");
				throw new WrongTimeFormatException("Invalid time!");
			}
			
			if ( isAm && Integer.parseInt(hour) == 12 ) {
				hour = "00";
			}
			if ( isPm && Integer.parseInt(hour) < 12 ) {
				hour = String.valueOf( Integer.parseInt(hour) + 12 );
			}
			
		} catch (NumberFormatException e) {
			throw new WrongTimeFormatException("Invalid time!");
		}
		
		return new String(hour + ":" + minute);
	}
	
	private static boolean isDateValid(int day, int month, int year) {
		assert( day > 0 );
		assert( month > 0 );
		assert( year > 0 );
		if ( year < 1900 ) {
			return false;
		}
		
		if ( month == 2 && isLeapYear(year) ) {
			return ( 29 >= day );
		}
		else {
			return ( NUMBER_OF_DAYS_IN_MONTH[month-1] >= day );
		}
	}
	
	private static boolean isTimeValid(int hour, int minute, boolean isAm, boolean isPm) {
		assert( hour >= 0 );
		assert( minute >= 0 );
		assert( !(isPm && isAm) );
		
		if ( isAm || isPm ) {
			if ( 1 > hour || hour > 12 ) {
				return false;
			}
		} else {
			if ( 0 > hour || hour > 24 ) {
				return false;
			}
		}
		
		if ( 0 > minute && minute > 59 ) {
			return false;
		}
		
		if ( hour == 24 && minute != 0 ) {
			return false;
		}
		
		return true;
	}

	private static boolean isLeapYear(int year) {
		return ( ( Utility.isDivisibleBy(year, 4) && !Utility.isDivisibleBy(year,100) ) || Utility.isDivisibleBy(year,400) );
	}
	
	private static Month assignMonth(String month) throws WrongTimeFormatException {
		try {
			return assignMonth(Integer.parseInt(month));
		} catch (NumberFormatException e){
			LOGGER.log(Level.SEVERE, "assignMonth NumberFormatException");
			throw new WrongTimeFormatException("Wrong format?");
		}
	}
	
	private static Month assignMonth(Integer month) throws WrongTimeFormatException {
		switch (month) {
		case 1:
			return Month.JANUARY;
		case 2:
			return Month.FEBRUARY;
		case 3:
			return Month.MARCH;
		case 4:
			return Month.APRIL;
		case 5:
			return Month.MAY;
		case 6:
			return Month.JUNE;
		case 7:
			return Month.JULY;
		case 8:
			return Month.AUGUST;
		case 9:
			return Month.SEPTEMBER;
		case 10:
			return Month.OCTOBER;
		case 11:
			return Month.NOVEMBER;
		case 12:
			return Month.DECEMBER;
		default:
			LOGGER.log(Level.SEVERE, month + "is invalid month");
		}
		throw new WrongTimeFormatException("Invalid input in assignMonth()");
	}

	static DayOfWeek calculateDayOfWeek(String stdDateFormat) throws WrongTimeFormatException {
		try {
			Date date = new SimpleDateFormat(FORMAT_DATE).parse(stdDateFormat);
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			//return cal.get(Calendar.DAY_OF_WEEK);
			return assignDayOfWeek(cal.get(Calendar.DAY_OF_WEEK));
		} catch (ParseException e) {
			e.printStackTrace();
			throw new WrongTimeFormatException("Unrecognised format for calculateDayOfWeek! Please use dd MMMM yyyy");
		}
	}
	
	private static DayOfWeek assignDayOfWeek(int dayOfWeek) throws WrongTimeFormatException {
		switch(dayOfWeek) {
		case 2:
			return DayOfWeek.MONDAY;
		case 3:
			return DayOfWeek.TUESDAY;
		case 4:
			return DayOfWeek.WEDNESDAY;
		case 5:
			return DayOfWeek.THURSDAY;
		case 6:
			return DayOfWeek.FRIDAY;
		case 7:
			return DayOfWeek.SATURDAY;
		case 1:
			return DayOfWeek.SUNDAY;
		default:
			LOGGER.log(Level.SEVERE, dayOfWeek + "is invalid day");
		}
		throw new WrongTimeFormatException("Invalid day in assignDayOfWeek!");
	}
	
	private static DayOfWeek assignDayOfWeek(String dayOfWeek) throws WrongTimeFormatException {
		
		if ( Utility.isStringInList(dayOfWeek, LIST_MON) ) {
			return DayOfWeek.MONDAY;
		} else if ( Utility.isStringInList(dayOfWeek, LIST_TUES) ) {
			return DayOfWeek.TUESDAY;
		} else if ( Utility.isStringInList(dayOfWeek, LIST_WED) ) {
			return DayOfWeek.WEDNESDAY;
		} else if ( Utility.isStringInList(dayOfWeek, LIST_THURS) ) {
			return DayOfWeek.THURSDAY;
		} else if ( Utility.isStringInList(dayOfWeek, LIST_FRI) ) {
			return DayOfWeek.FRIDAY;
		} else if ( Utility.isStringInList(dayOfWeek, LIST_SAT) ) {
			return DayOfWeek.SATURDAY;
		} else if ( Utility.isStringInList(dayOfWeek, LIST_SUN) ) {
			return DayOfWeek.SUNDAY;
		} else {
			LOGGER.log(Level.SEVERE, "failed to assign " + dayOfWeek);
			throw new WrongTimeFormatException("failed to assign " + dayOfWeek);
		}
	}
	
	private static Calendar getUpdatedCal() {
		cal = Calendar.getInstance(Locale.ENGLISH);
		return cal;
	}
	
	public static String getEndTime(String primitiveStartDate, String primitiveStartTime, String duration) throws WrongTimeFormatException {
		String stdStartDate = parseDate(primitiveStartDate);
		String stdStartTime = parseTime(primitiveStartTime);
		int minutes = parseDurationIntoMinutes(duration);
		
		try {
			Date date = new SimpleDateFormat(FORMAT_DATE_AND_TIME).parse(stdStartDate + "," + stdStartTime);
			
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			
			cal.add(Calendar.MINUTE, minutes);
			
			System.out.println(new SimpleDateFormat(FORMAT_DATE_AND_TIME).format(cal.getTime()));
			return new SimpleDateFormat(FORMAT_DATE_AND_TIME).format(cal.getTime());
		} catch (ParseException e) {
			e.printStackTrace();
			LOGGER.log(Level.SEVERE, "problem with SimpleDateFormat parse");
			throw new WrongTimeFormatException("problem with SimpleDateFormat parse!");
		}
	}

	private static int parseDurationIntoMinutes(String duration)
			throws WrongTimeFormatException {
		assert (duration != null);
		assert (duration != "");
		final String[] LIST_OF_DURATION = { "w", "week", "weeks", "d", "day",
				"days", "h", "hr", "hrs", "hour", "hours", "m", "min", "mins" };
		int durationInMinutes = 0;

		// remove all symbols
		duration = duration.replaceAll("[-._/]", "");

		duration = addSpaceBetweenAlphaNumerics(duration);

		StringTokenizer st = new StringTokenizer(duration, " ");
		int numOfTokens = st.countTokens();
		if (numOfTokens % 2 != 0) {
			LOGGER.log(Level.SEVERE, "Invalid duration");
			throw new WrongTimeFormatException("Invalid duration");
		}

		String multiplier, base;

		for (int i = 0; i < numOfTokens; i = i + 2) {
			multiplier = st.nextToken();
			base = st.nextToken();

			if ((Utility.isInteger(multiplier) || Utility.isStringInList(base,
					LIST_OF_DURATION)) == false) {
				LOGGER.log(Level.SEVERE, "Wrong format for duration!");
				throw new WrongTimeFormatException("Wrong format for duration!");
			}

			base = base.substring(0, 1);
			int baseMinute;

			switch (base) {
			case "w":
				baseMinute = 10080;
				break;
			case "d":
				baseMinute = 1440;
				break;
			case "h":
				baseMinute = 60;
				break;
			case "m":
				baseMinute = 1;
				break;
			default:
				LOGGER.log(Level.SEVERE, "Something wrong with base in parseDurationIntoMinutes");
				throw new WrongTimeFormatException("Something wrong with base in parseDurationIntoMinutes");
			}
			try {
				durationInMinutes += baseMinute * Integer.parseInt(multiplier);
			} catch (NumberFormatException e) {
				throw new WrongTimeFormatException("Something wrong with base in parseDurationIntoMinutes");
			}
		}

		return durationInMinutes;
	}

	private static String addSpaceBetweenAlphaNumerics(String duration) {
		duration = duration.replaceAll("(?x) (?<= \\pL ) (?= \\pN ) | (?<= \\pN ) (?= \\pL )", " ");
		return duration;
	}

	public static boolean isValidStartEndDate(String startDate, String endDate) {
		return isValidSchedule(startDate, "00:00", endDate, "23:59");
	}

	public static boolean isValidSchedule(String startDate, String startTime, String endDate, String endTime) {
		String start = startDate + "," + startTime;
		String end = endDate + "," + endTime;
		SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_DATE_AND_TIME);
		
		Calendar startCalendar = Calendar.getInstance();
		Calendar endCalendar = Calendar.getInstance();
		
		try {
			startCalendar.setTime(formatter.parse(start));
			endCalendar.setTime(formatter.parse(end));
			
			return startCalendar.before(endCalendar);
		} catch (ParseException e) {
			LOGGER.log(Level.SEVERE, "Error checking if schedule is valid!");
			return false;
		}
	}
	
	public static boolean isCurrentEarlierThanDate(String stdStartDateTime) {
		SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_DATE_AND_TIME);
		Calendar inputTime = Calendar.getInstance();
		Calendar currTime = Calendar.getInstance();
		
		try {
			inputTime.setTime(formatter.parse(stdStartDateTime));
			currTime = getUpdatedCal();
			
			return currTime.before(inputTime);
		} catch (ParseException e) {
			LOGGER.log(Level.SEVERE, "Error checking if schedule is later than current time");
			return false;
		}
	}
	
	public static boolean isDate(String stringToInvestigate) {
		try {
			parseDate(stringToInvestigate);
			return true;
		} catch (WrongTimeFormatException e) {
			return false;
		}
	}
}
